diff --git a/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts b/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts index 24073c11d..c98dc0421 100644 --- a/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts +++ b/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts @@ -1,25 +1,25 @@ -import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {Component} from '@angular/core'; import {MediaChange, MediaObserver} from '@angular/flex-layout'; import {Observable} from 'rxjs'; @Component({ selector: 'media-query-status', template: ` -
- {{ extractQuery(event) }} +
+ Active MediaQuery(s): +
`, styleUrls: ['./media-query-status.component.scss'], - changeDetection : ChangeDetectionStrategy.OnPush }) export class MediaQueryStatusComponent { - media$: Observable; + media$: Observable; - constructor(mediaObserver: MediaObserver) { - this.media$ = mediaObserver.media$; - } - - extractQuery(change: MediaChange): string { - return change ? `'${change.mqAlias}' = (${change.mediaQuery})` : ''; + constructor(media: MediaObserver) { + this.media$ = media.asObservable(); } } diff --git a/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts b/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts index 9d2b6b291..3eeb58e8f 100644 --- a/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts +++ b/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts @@ -17,13 +17,13 @@ export class ResponsiveRowColumnComponent implements OnDestroy { }; isVisible = true; - private activeMQC: MediaChange; + private activeMQC: MediaChange[]; private subscription: Subscription; - constructor(mediaObserver: MediaObserver) { - this.subscription = mediaObserver.media$ - .subscribe((e: MediaChange) => { - this.activeMQC = e; + constructor(mediaService: MediaObserver) { + this.subscription = mediaService.asObservable() + .subscribe((events: MediaChange[]) => { + this.activeMQC = events; }); } @@ -32,16 +32,18 @@ export class ResponsiveRowColumnComponent implements OnDestroy { } toggleLayoutFor(col: number) { - switch (col) { - case 1: - const set1 = `firstCol${this.activeMQC ? this.activeMQC.suffix : ''}`; - this.cols[set1] = (this.cols[set1] === 'column') ? 'row' : 'column'; - break; + this.activeMQC.forEach((change: MediaChange) => { + switch (col) { + case 1: + const set1 = `firstCol${change ? change.suffix : ''}`; + this.cols[set1] = (this.cols[set1] === 'column') ? 'row' : 'column'; + break; - case 2: - const set2 = 'secondCol'; - this.cols[set2] = (this.cols[set2] === 'row') ? 'column' : 'row'; - break; - } + case 2: + const set2 = 'secondCol'; + this.cols[set2] = (this.cols[set2] === 'row') ? 'column' : 'row'; + break; + } + }); } } diff --git a/src/lib/core/add-alias.ts b/src/lib/core/add-alias.ts index f098bb7bf..af61c0572 100644 --- a/src/lib/core/add-alias.ts +++ b/src/lib/core/add-alias.ts @@ -7,15 +7,18 @@ */ import {MediaChange} from './media-change'; import {BreakPoint} from './breakpoints/break-point'; -import {extendObject} from '../utils/object-extend'; /** * For the specified MediaChange, make sure it contains the breakpoint alias * and suffix (if available). */ export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange { - return extendObject(dest || {}, source ? { - mqAlias: source.alias, - suffix: source.suffix - } : {}); + dest = dest ? dest.clone() : new MediaChange(); + if (source) { + dest.mqAlias = source.alias; + dest.mediaQuery = source.mediaQuery; + dest.suffix = source.suffix as string; + dest.priority = source.priority as number; + } + return dest; } diff --git a/src/lib/core/match-media/match-media.spec.ts b/src/lib/core/match-media/match-media.spec.ts index b466e9073..1a1f9b3f7 100644 --- a/src/lib/core/match-media/match-media.spec.ts +++ b/src/lib/core/match-media/match-media.spec.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import {TestBed, inject} from '@angular/core/testing'; +import {BreakPoint} from '@angular/flex-layout/core'; +import {Subscription} from 'rxjs'; import {MediaChange} from '../media-change'; import {MockMatchMedia, MockMatchMediaProvider} from './mock/mock-match-media'; @@ -16,7 +18,6 @@ import {MediaObserver} from '../media-observer/media-observer'; describe('match-media', () => { let breakPoints: BreakPointRegistry; let mediaController: MockMatchMedia; - let mediaObserver: MediaObserver; beforeEach(() => { // Configure testbed to prepare services @@ -31,11 +32,11 @@ describe('match-media', () => { _breakPoints: BreakPointRegistry) => { breakPoints = _breakPoints; mediaController = _matchMedia; // inject only to manually activate mediaQuery ranges - mediaObserver = _mediaObserver; })); afterEach(() => { - mediaController.clearAll(); + mediaController.clearAll(); + mediaController.useOverlaps = false; }); it('can observe the initial, default activation for mediaQuery == "all". ', () => { @@ -101,24 +102,27 @@ describe('match-media', () => { }); describe('match-media-observable', () => { + const watchMedia = (alias: string, observer: (value: MediaChange) => void): Subscription => { + return mediaController + .observe(alias ? [alias] : []) + .subscribe(observer); + }; it('can observe an existing activation', () => { let current: MediaChange = new MediaChange(); let bp = breakPoints.findByAlias('md')!; - mediaController.activate(bp.mediaQuery); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { - current = change; - }); + const onChange = (change: MediaChange) => current = change; + const subscription = watchMedia('md', onChange); + mediaController.activate(bp.mediaQuery); expect(current.mediaQuery).toEqual(bp.mediaQuery); subscription.unsubscribe(); }); it('can observe the initial, default activation for mediaQuery == "all". ', () => { let current: MediaChange = new MediaChange(); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { - current = change; - }); + const onChange = (change: MediaChange) => current = change; + const subscription = watchMedia('', onChange); expect(current.mediaQuery).toEqual('all'); subscription.unsubscribe(); @@ -126,13 +130,13 @@ describe('match-media', () => { it('can observe custom mediaQuery ranges', () => { let current: MediaChange = new MediaChange(); - let customQuery = 'screen and (min-width: 610px) and (max-width: 620px)'; - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { - current = change; - }); + const customQuery = 'screen and (min-width: 610px) and (max-width: 620px)'; + const onChange = (change: MediaChange) => current = change; + const subscription = watchMedia(customQuery, onChange); mediaController.useOverlaps = true; - let activated = mediaController.activate(customQuery); + const activated = mediaController.activate(customQuery); + expect(activated).toEqual(true); expect(current.mediaQuery).toEqual(customQuery); @@ -141,46 +145,47 @@ describe('match-media', () => { it('can observe registered breakpoint activations', () => { let current: MediaChange = new MediaChange(); - let bp = breakPoints.findByAlias('md') !; - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { - current = change; - }); + const onChange = (change: MediaChange) => current = change; + const subscription = watchMedia('md', onChange); + let bp = breakPoints.findByAlias('md') !; let activated = mediaController.activate(bp.mediaQuery); - expect(activated).toEqual(true); + expect(activated).toEqual(true); expect(current.mediaQuery).toEqual(bp.mediaQuery); subscription.unsubscribe(); }); /** - * Only the MediaObserver ignores de-activations; * MediaMonitor and MatchMedia report both activations and de-activations! + * Only the MediaObserver ignores de-activations; */ - it('ignores mediaQuery de-activations', () => { - let activationCount = 0; - let deactivationCount = 0; - - mediaObserver.filterOverlaps = false; - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { + it('reports mediaQuery de-activations', () => { + const lookupMediaQuery = (alias: string) => { + const bp: BreakPoint = breakPoints.findByAlias(alias) as BreakPoint; + return bp.mediaQuery; + }; + let activationCount = 0, deactivationCount = 0; + let subscription = watchMedia('', (change: MediaChange) => { if (change.matches) { - ++activationCount; + activationCount += 1; } else { - ++deactivationCount; + deactivationCount += 1; } }); - mediaController.activate(breakPoints.findByAlias('md')!.mediaQuery); - mediaController.activate(breakPoints.findByAlias('gt-md')!.mediaQuery); - mediaController.activate(breakPoints.findByAlias('lg')!.mediaQuery); + mediaController.activate(lookupMediaQuery('md')); + mediaController.activate(lookupMediaQuery('gt-md')); + mediaController.activate(lookupMediaQuery('lg')); // 'all' mediaQuery is already active; total count should be (3) expect(activationCount).toEqual(4); - expect(deactivationCount).toEqual(0); + expect(deactivationCount).toEqual(2); subscription.unsubscribe(); }); }); }); + diff --git a/src/lib/core/match-media/match-media.ts b/src/lib/core/match-media/match-media.ts index dfc9d70c3..a66ce93b2 100644 --- a/src/lib/core/match-media/match-media.ts +++ b/src/lib/core/match-media/match-media.ts @@ -32,6 +32,19 @@ export class MatchMedia { @Inject(DOCUMENT) protected _document: any) { } + /** + * Publish list of all current activations + */ + get activations(): string[] { + const results: string[] = []; + this._registry.forEach((mql: MediaQueryList, key: string) => { + if (mql.matches) { + results.push(key); + } + }); + return results; + } + /** * For the specified mediaQuery? */ @@ -60,7 +73,7 @@ export class MatchMedia { * subscribers of notifications. */ observe(mqList?: string[], filterOthers = false): Observable { - if (mqList) { + if (mqList && mqList.length) { const matchMedia$: Observable = this._observable$.pipe( filter((change: MediaChange) => { return !filterOthers ? true : (mqList.indexOf(change.mediaQuery) > -1); diff --git a/src/lib/core/media-change.ts b/src/lib/core/media-change.ts index 9e9b1e91c..3bdc2b033 100644 --- a/src/lib/core/media-change.ts +++ b/src/lib/core/media-change.ts @@ -23,7 +23,8 @@ export class MediaChange { constructor(public matches = false, public mediaQuery = 'all', public mqAlias = '', - public suffix = '') { + public suffix = '', + public priority = 0) { } /** Create an exact copy of the MediaChange */ diff --git a/src/lib/core/media-observer/media-observer.spec.ts b/src/lib/core/media-observer/media-observer.spec.ts index 3b6066c0f..ce1dbfbec 100644 --- a/src/lib/core/media-observer/media-observer.spec.ts +++ b/src/lib/core/media-observer/media-observer.spec.ts @@ -5,8 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {TestBed, inject} from '@angular/core/testing'; -import {filter, map} from 'rxjs/operators'; +import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing'; +import {Observable} from 'rxjs'; +import {filter} from 'rxjs/operators'; import {BreakPoint} from '../breakpoints/break-point'; import {BREAKPOINTS} from '../breakpoints/break-points-token'; @@ -19,8 +20,13 @@ import {DEFAULT_CONFIG, LAYOUT_CONFIG} from '../tokens/library-config'; describe('media-observer', () => { let knownBreakPoints: BreakPoint[] = []; + let media$: Observable; let mediaObserver: MediaObserver; let mediaController: MockMatchMedia; + const activateQuery = (alias: string) => { + mediaController.activate(alias); + tick(100); // Since MediaObserver has 50ms debounceTime + }; describe('with default BreakPoints', () => { beforeEach(() => { @@ -35,10 +41,13 @@ describe('media-observer', () => { knownBreakPoints = breakpoints; mediaObserver = _mediaObserver; mediaController = _mediaController; + + media$ = _mediaObserver.media$; })); afterEach(() => { mediaController.clearAll(); + mediaController.useOverlaps = false; }); let findMediaQuery: (alias: string) => string = (alias) => { @@ -48,7 +57,7 @@ describe('media-observer', () => { }, null) as string || NOT_FOUND; }; it('can supports the `.isActive()` API', () => { - expect(mediaObserver).toBeDefined(); + expect(media$).toBeDefined(); // Activate mediaQuery associated with 'md' alias mediaController.activate('md'); @@ -61,44 +70,45 @@ describe('media-observer', () => { mediaController.clearAll(); }); - it('can supports RxJS operators', () => { + it('can supports RxJS operators', fakeAsync(() => { let count = 0, - subscription = mediaObserver.media$.pipe( - filter((change: MediaChange) => change.mqAlias == 'md'), - map((change: MediaChange) => change.mqAlias) - ).subscribe(_ => { + onlyMd = (change: MediaChange) => (change.mqAlias == 'md'), + subscription = media$ + .pipe(filter(onlyMd)) + .subscribe(_ => { count += 1; }); // Activate mediaQuery associated with 'md' alias - mediaController.activate('sm'); + activateQuery('sm'); expect(count).toEqual(0); - mediaController.activate('md'); + activateQuery('md'); expect(count).toEqual(1); - mediaController.activate('lg'); + activateQuery('lg'); expect(count).toEqual(1); - mediaController.activate('md'); + activateQuery('md'); expect(count).toEqual(2); - mediaController.activate('gt-md'); - mediaController.activate('gt-lg'); - mediaController.activate('invalid'); + activateQuery('gt-md'); + activateQuery('gt-lg'); + activateQuery('invalid'); expect(count).toEqual(2); subscription.unsubscribe(); - mediaController.clearAll(); - }); + })); - it('can subscribe to built-in mediaQueries', () => { + it('can subscribe to built-in mediaQueries', fakeAsync(() => { let current: MediaChange = new MediaChange(true); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { + let subscription = media$.subscribe((change: MediaChange) => { current = change; }); - expect(mediaObserver).toBeDefined(); + expect(media$).toBeDefined(); + + tick(100); // Confirm initial match is for 'all' expect(current).toBeDefined(); @@ -111,63 +121,61 @@ describe('media-observer', () => { mediaObserver.filterOverlaps = false; // Activate mediaQuery associated with 'md' alias - mediaController.activate('md'); + activateQuery('md'); expect(current.mediaQuery).toEqual(findMediaQuery('md')); - mediaController.activate('gt-lg'); + activateQuery('gt-lg'); expect(current.mediaQuery).toEqual(findMediaQuery('gt-lg')); - mediaController.activate('unknown'); + activateQuery('unknown'); expect(current.mediaQuery).toEqual(findMediaQuery('gt-lg')); } finally { mediaController.autoRegisterQueries = true; subscription.unsubscribe(); - - mediaController.clearAll(); } - }); + })); - it('can `.unsubscribe()` properly', () => { + it('can `.unsubscribe()` properly', fakeAsync(() => { let current: MediaChange = new MediaChange(true); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { + let subscription = media$.subscribe((change: MediaChange) => { current = change; }); // Activate mediaQuery associated with 'md' alias - mediaController.activate('md'); + activateQuery('md'); expect(current.mediaQuery).toEqual(findMediaQuery('md')); // Un-subscribe subscription.unsubscribe(); - mediaController.activate('lg'); + activateQuery('lg'); expect(current.mqAlias).toBe('md'); - mediaController.activate('xs'); + activateQuery('xs'); expect(current.mqAlias).toBe('md'); mediaController.clearAll(); - }); + })); - it('can observe a startup activation of XS', () => { + it('can observe a startup activation of XS', fakeAsync(() => { let current: MediaChange = new MediaChange(true); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { + let subscription = media$.subscribe((change: MediaChange) => { current = change; }); // Activate mediaQuery associated with 'md' alias - mediaController.activate('xs'); + activateQuery('xs'); expect(current.mediaQuery).toEqual(findMediaQuery('xs')); // Un-subscribe subscription.unsubscribe(); - mediaController.activate('lg'); + activateQuery('lg'); expect(current.mqAlias).toBe('xs'); mediaController.clearAll(); - }); + })); }); describe('with custom BreakPoints', () => { @@ -195,34 +203,36 @@ describe('media-observer', () => { knownBreakPoints = breakpoints; mediaObserver = _mediaObserver; mediaController = _mediaController; + + media$ = _mediaObserver.media$; })); afterEach(() => { mediaController.clearAll(); }); - it('can activate custom alias with custom mediaQueries', () => { + it('can activate custom alias with custom mediaQueries', fakeAsync(() => { let current: MediaChange = new MediaChange(true); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { - current = change; - }); + let subscription = mediaObserver.asObservable() + .subscribe((changes: MediaChange[]) => { + current = changes[0]; + }); // Activate mediaQuery associated with 'md' alias - mediaController.activate('sm'); + activateQuery('sm'); expect(current.mediaQuery).toEqual(smMediaQuery); // MediaObserver will not announce print events // unless a printAlias layout has been configured. - mediaController.activate('slate.xl'); + activateQuery('slate.xl'); expect(current.mediaQuery).toEqual(superXLQuery); - mediaController.activate('tablet-gt-xs'); + activateQuery('tablet-gt-xs'); expect(current.mqAlias).toBe('tablet-gt-xs'); expect(current.mediaQuery).toBe(gtXsMediaQuery); subscription.unsubscribe(); - mediaController.clearAll(); - }); + })); }); describe('with layout "print" configured', () => { @@ -249,32 +259,32 @@ describe('media-observer', () => { knownBreakPoints = breakpoints; mediaObserver = _mediaObserver; mediaController = _mediaController; + + media$ = _mediaObserver.media$; })); - it('can activate when configured with "md" alias', () => { + it('can activate when configured with "md" alias', fakeAsync(() => { let current: MediaChange = new MediaChange(true); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { + let subscription = media$.subscribe((change: MediaChange) => { current = change; }); try { - - mediaController.activate('lg'); + activateQuery('lg'); // Activate mediaQuery associated with 'md' alias - mediaController.activate('print'); + activateQuery('print'); expect(current.mqAlias).toBe('md'); expect(current.mediaQuery).toEqual(mdMediaQuery); - mediaController.activate('sm'); + activateQuery('sm'); expect(current.mqAlias).toBe('sm'); } finally { - mediaController.clearAll(); subscription.unsubscribe(); } - }); + })); }); describe('with layout print NOT configured', () => { @@ -294,29 +304,30 @@ describe('media-observer', () => { knownBreakPoints = breakpoints; mediaObserver = _mediaObserver; mediaController = _mediaController; + + media$ = _mediaObserver.media$; })); afterEach(() => { mediaController.clearAll(); }); - it('will skip print activation without alias', () => { + it('will skip print activation without alias', fakeAsync(() => { let current: MediaChange = new MediaChange(true); - let subscription = mediaObserver.media$.subscribe((change: MediaChange) => { + let subscription = media$.subscribe((change: MediaChange) => { current = change; }); try { - - mediaController.activate('sm'); + activateQuery('sm'); expect(current.mqAlias).toBe('sm'); // Activate mediaQuery associated with 'md' alias - mediaController.activate('print'); + activateQuery('print'); expect(current.mqAlias).toBe('sm'); expect(current.mediaQuery).toEqual(smMediaQuery); - mediaController.activate('xl'); + activateQuery('xl'); expect(current.mqAlias).toBe('xl'); } finally { @@ -324,6 +335,6 @@ describe('media-observer', () => { mediaController.clearAll(); } - }); + })); }); }); diff --git a/src/lib/core/media-observer/media-observer.ts b/src/lib/core/media-observer/media-observer.ts index 883598ce2..a64bded95 100644 --- a/src/lib/core/media-observer/media-observer.ts +++ b/src/lib/core/media-observer/media-observer.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; -import {filter, map} from 'rxjs/operators'; +import {Observable, of} from 'rxjs'; +import {debounceTime, filter, map, switchMap} from 'rxjs/operators'; import {mergeAlias} from '../add-alias'; import {MediaChange} from '../media-change'; @@ -16,21 +16,19 @@ import {PrintHook} from '../media-marshaller/print-hook'; import {BreakPointRegistry, OptionalBreakPoint} from '../breakpoints/break-point-registry'; /** - * Class internalizes a MatchMedia service and exposes an Observable interface. - - * This exposes an Observable with a feature to subscribe to mediaQuery - * changes and a validator method (`isActive()`) to test if a mediaQuery (or alias) is - * currently active. + * MediaObserver enables applications to listen for 1..n mediaQuery activations and to determine + * if a mediaQuery is currently activated. * - * !! Only mediaChange activations (not de-activations) are announced by the MediaObserver + * Since a breakpoint change will first deactivate 1...n mediaQueries and then possibly activate + * 1..n mediaQueries, the MediaObserver will debounce notifications and report ALL *activations* + * in 1 event notification. The reported activations will be sorted in descending priority order. * * This class uses the BreakPoint Registry to inject alias information into the raw MediaChange * notification. For custom mediaQuery notifications, alias information will not be injected and * those fields will be ''. * - * !! This is not an actual Observable. It is a wrapper of an Observable used to publish additional - * methods like `isActive(). To access the Observable and use RxJS operators, use - * `.media$` with syntax like mediaObserver.media$.map(....). + * Note: Developers should note that only mediaChange activations (not de-activations) + * are announced by the MediaObserver. * * @usage * @@ -43,41 +41,70 @@ import {BreakPointRegistry, OptionalBreakPoint} from '../breakpoints/break-point * status: string = ''; * * constructor(mediaObserver: MediaObserver) { - * const onChange = (change: MediaChange) => { - * this.status = change ? `'${change.mqAlias}' = (${change.mediaQuery})` : ''; - * }; + * const media$ = mediaObserver.asObservable().pipe( + * filter((changes: MediaChange[]) => true) // silly noop filter + * ); * - * // Subscribe directly or access observable to use filter/map operators - * // e.g. mediaObserver.media$.subscribe(onChange); + * media$.subscribe((changes: MediaChange[]) => { + * let status = ''; + * changes.forEach( change => { + * status += `'${change.mqAlias}' = (${change.mediaQuery})
` ; + * }); + * this.status = status; + * }); * - * mediaObserver.media$() - * .pipe( - * filter((change: MediaChange) => true) // silly noop filter - * ).subscribe(onChange); * } * } */ @Injectable({providedIn: 'root'}) export class MediaObserver { + /** - * Whether to announce gt- breakpoint activations + * @deprecated Use `asObservable()` instead. + * @breaking-change 7.0.0-beta.24 + * @deletion-target v7.0.0-beta.25 */ - filterOverlaps = true; - readonly media$: Observable; + get media$(): Observable { + return this._media$.pipe( + filter((changes: MediaChange[]) => changes.length > 0), + map((changes: MediaChange[]) => changes[0]) + ); + } + + /** Filter MediaChange notifications for overlapping breakpoints */ + filterOverlaps = false; constructor(protected breakpoints: BreakPointRegistry, - protected mediaWatcher: MatchMedia, - protected hook: PrintHook) { - this.media$ = this.watchActivations(); + protected matchMedia: MatchMedia, + protected hook: PrintHook) { + this._media$ = this.watchActivations(); + } + + + // ************************************************ + // Public Methods + // ************************************************ + + /** + * Observe changes to current activation 'list' + */ + asObservable(): Observable { + return this._media$; } /** - * Test if specified query/alias is active. + * Allow programmatic query to determine if specified query/alias is active. */ isActive(alias: string): boolean { - return this.mediaWatcher.isActive(this.toMediaQuery(alias)); + const query = toMediaQuery(alias, this.breakpoints); + return this.matchMedia.isActive(query); } + /** + * Subscribers to activation list can use this function to easily exclude overlaps + */ + + // ************************************************ // Internal Methods // ************************************************ @@ -93,53 +120,79 @@ export class MediaObserver { } /** - * Prepare internal observable + * Only pass/announce activations (not de-activations) + * + * Since multiple-mediaQueries can be activation in a cycle, + * gather all current activations into a single list of changes to observers + * + * Inject associated (if any) alias information into the MediaChange event + * - Exclude mediaQuery activations for overlapping mQs. List bounded mQ ranges only + * - Exclude print activations that do not have an associated mediaQuery * * NOTE: the raw MediaChange events [from MatchMedia] do not * contain important alias information; as such this info * must be injected into the MediaChange */ - private buildObservable(mqList: string[]): Observable { - const locator = this.breakpoints; - const onlyActivations = (change: MediaChange) => change.matches; - const excludeUnknown = (change: MediaChange) => change.mediaQuery !== ''; - const excludeCustomPrints = (change: MediaChange) => !change.mediaQuery.startsWith('print'); - const excludeOverlaps = (change: MediaChange) => { - const bp = locator.findByQuery(change.mediaQuery); - return !bp ? true : !(this.filterOverlaps && bp.overlapping); + private buildObservable(mqList: string[]): Observable { + const hasChanges = (changes: MediaChange[]) => { + const isValidQuery = (change: MediaChange) => (change.mediaQuery.length > 0); + return (changes.filter(isValidQuery).length > 0); }; - const replaceWithPrintAlias = (change: MediaChange) => { - if (this.hook.isPrintEvent(change)) { - // replace with aliased substitute (if configured) - return this.hook.updateEvent(change); - } - let bp: OptionalBreakPoint = locator.findByQuery(change.mediaQuery); - return mergeAlias(change, bp); + const excludeOverlaps = (changes: MediaChange[]) => { + return !this.filterOverlaps ? changes : changes.filter(change => { + const bp = this.breakpoints.findByQuery(change.mediaQuery); + return !bp ? true : !bp.overlapping; + }); }; /** - * Only pass/announce activations (not de-activations) - * - * Inject associated (if any) alias information into the MediaChange event - * - Exclude mediaQuery activations for overlapping mQs. List bounded mQ ranges only - * - Exclude print activations that do not have an associated mediaQuery */ - return this.mediaWatcher.observe(this.hook.withPrintQuery(mqList)) + return this.matchMedia + .observe(this.hook.withPrintQuery(mqList)) .pipe( - filter(onlyActivations), - filter(excludeOverlaps), - map(replaceWithPrintAlias), - filter(excludeCustomPrints), - filter(excludeUnknown) + filter((change: MediaChange) => change.matches), + debounceTime(10), + switchMap(_ => of(this.findAllActivations())), + map(excludeOverlaps), + filter(hasChanges) ); } /** - * Find associated breakpoint (if any) + * Find all current activations and prepare single list of activations + * sorted by descending priority. */ - private toMediaQuery(query: string) { - const locator = this.breakpoints; - const bp = locator.findByAlias(query) || locator.findByQuery(query); - return bp ? bp.mediaQuery : query; + private findAllActivations(): MediaChange[] { + const mergeMQAlias = (change: MediaChange) => { + let bp: OptionalBreakPoint = this.breakpoints.findByQuery(change.mediaQuery); + return mergeAlias(change, bp); + }; + const replaceWithPrintAlias = (change: MediaChange) => { + return this.hook.isPrintEvent(change) ? this.hook.updateEvent(change) : change; + }; + + return this.matchMedia + .activations + .map(query => new MediaChange(true, query)) + .map(replaceWithPrintAlias) + .map(mergeMQAlias) + .sort(sortChangesByPriority); } + + private _media$: Observable; +} + +/** + * Find associated breakpoint (if any) + */ +function toMediaQuery(query: string, locator: BreakPointRegistry) { + const bp = locator.findByAlias(query) || locator.findByQuery(query); + return bp ? bp.mediaQuery : query; +} + +/** HOF to sort the breakpoints by priority */ +export function sortChangesByPriority(a: MediaChange, b: MediaChange): number { + const priorityA = a ? a.priority || 0 : 0; + const priorityB = b ? b.priority || 0 : 0; + return priorityB - priorityA; } diff --git a/src/lib/core/public-api.ts b/src/lib/core/public-api.ts index 5d43b586a..1bbadec2b 100644 --- a/src/lib/core/public-api.ts +++ b/src/lib/core/public-api.ts @@ -6,11 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './browser-provider'; export * from './module'; +export * from './browser-provider'; export * from './media-change'; export * from './stylesheet-map/index'; export * from './tokens/index'; +export * from './add-alias'; export * from './base/index'; export * from './breakpoints/index'; diff --git a/tslint.json b/tslint.json index c87b09e6b..5a2e904c5 100644 --- a/tslint.json +++ b/tslint.json @@ -25,7 +25,7 @@ "no-trailing-whitespace": true, "no-bitwise": true, "no-shadowed-variable": true, - "no-unused-expression": true, + "no-unused-expression": [true], "no-var-keyword": true, "member-access": [true, "no-public"], "no-debugger": true,