Skip to content

Commit

Permalink
fix(ObservableMedia): startup should propagate lastReplay value prope…
Browse files Browse the repository at this point in the history
…rly (#313)

Fixes #245, #275, #303
  • Loading branch information
ThomasBurleson authored and kara committed Jun 13, 2017
1 parent 771f2c9 commit 00ac57a
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 56 deletions.
4 changes: 3 additions & 1 deletion src/demo-app/app/shared/media-query-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export class MediaQueryStatus implements OnDestroy {
private _watcher : Subscription;
activeMediaQuery : string;

constructor(media$ : ObservableMedia) { this.watchMediaQueries(media$); }
constructor(media$ : ObservableMedia) {
this.watchMediaQueries(media$);
}

ngOnDestroy() {
this._watcher.unsubscribe();
Expand Down
14 changes: 14 additions & 0 deletions src/lib/media-query/breakpoints/break-point-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export class BreakPointRegistry {
return [...this._registry];
}

/**
* Accessor to sorted list used for registration with matchMedia API
*
* NOTE: During breakpoint registration, we want to register the overlaps FIRST
* so the non-overlaps will trigger the MatchMedia:BehaviorSubject last!
* And the largest, non-overlap, matching breakpoint should be the lastReplay value
*/
get sortedItems(): BreakPoint[] {
let overlaps = this._registry.filter(it => it.overlapping === true);
let nonOverlaps = this._registry.filter(it => it.overlapping !== true);

return [...overlaps, ...nonOverlaps];
}

/**
* Search breakpoints by alias (e.g. gt-xs)
*/
Expand Down
5 changes: 2 additions & 3 deletions src/lib/media-query/match-media.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,12 @@ describe('match-media', () => {
});


it('can observe only a specific custom mediaQuery ranges', () => {
it('can observe an array of custom mediaQuery ranges', () => {
let current: MediaChange, activated;
let query1 = "screen and (min-width: 610px) and (max-width: 620px)";
let query2 = "(min-width: 730px) and (max-width: 950px)";

matchMedia.registerQuery(query1);
matchMedia.registerQuery(query2);
matchMedia.registerQuery([query1, query2]);

let subscription = matchMedia.observe(query1).subscribe((change: MediaChange) => {
current = change;
Expand Down
99 changes: 63 additions & 36 deletions src/lib/media-query/match-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,56 +76,59 @@ export class MatchMedia {
observe(mediaQuery?: string): Observable<MediaChange> {
this.registerQuery(mediaQuery);

return this._observable$.filter((change: MediaChange) => {
return mediaQuery ? (change.mediaQuery === mediaQuery) : true;
});
return this._observable$
.filter((change: MediaChange) => {
return mediaQuery ? (change.mediaQuery === mediaQuery) : true;
});
}

/**
* Based on the BreakPointRegistry provider, register internal listeners for each unique
* mediaQuery. Each listener emits specific MediaChange data to observers
*/
registerQuery(mediaQuery: string) {
if (mediaQuery) {
let mql = this._registry.get(mediaQuery);
let onMQLEvent = (e: MediaQueryList) => {
this._zone.run(() => {
let change = new MediaChange(e.matches, mediaQuery);
this._source.next(change);
});
};
registerQuery(mediaQuery: string | string[]) {
let list = normalizeQuery(mediaQuery);

if (list.length > 0) {
prepareQueryCSS(list);

list.forEach(query => {
let mql = this._registry.get(query);
let onMQLEvent = (e: MediaQueryList) => {
this._zone.run(() => {
let change = new MediaChange(e.matches, query);
this._source.next(change);
});
};

if (!mql) {
mql = this._buildMQL(mediaQuery);
mql.addListener(onMQLEvent);
this._registry.set(mediaQuery, mql);
}
if (!mql) {
mql = this._buildMQL(query);
mql.addListener(onMQLEvent);
this._registry.set(query, mql);
}

if (mql.matches) {
onMQLEvent(mql); // Announce activate range for initial subscribers
}
if (mql.matches) {
onMQLEvent(mql); // Announce activate range for initial subscribers
}
});
}

}

/**
* Call window.matchMedia() to build a MediaQueryList; which
* supports 0..n listeners for activation/deactivation
*/
protected _buildMQL(query: string): MediaQueryList {
prepareQueryCSS(query);

let canListen = !!(<any>window).matchMedia('all').addListener;
return canListen ? (<any>window).matchMedia(query) : <MediaQueryList>{
matches: query === 'all' || query === '',
media: query,
addListener: () => {
},
removeListener: () => {
}
};
matches: query === 'all' || query === '',
media: query,
addListener: () => {
},
removeListener: () => {
}
};
}

}

/**
Expand All @@ -135,31 +138,55 @@ export class MatchMedia {
const ALL_STYLES = {};

/**
* For Webkit engines that only trigger the MediaQueryListListener
* For Webkit engines that only trigger the MediaQueryList Listener
* when there is at least one CSS selector for the respective media query.
*
* @param query string The mediaQuery used to create a faux CSS selector
*
*/
function prepareQueryCSS(query) {
if (!ALL_STYLES[query]) {
function prepareQueryCSS(mediaQueries: string[]) {
let list = mediaQueries.filter(it => !ALL_STYLES[it]);
if (list.length > 0) {
let query = list.join(", ");
try {
let style = document.createElement('style');

style.setAttribute('type', 'text/css');
if (!style['styleSheet']) {
let cssText = `@media ${query} {.fx-query-test{ }}`;
let cssText = `/*
@angular/flex-layout - workaround for possible browser quirk with mediaQuery listeners
see http://bit.ly/2sd4HMP
*/
@media ${query} {.fx-query-test{ }}`;
style.appendChild(document.createTextNode(cssText));
}

document.getElementsByTagName('head')[0].appendChild(style);

// Store in private global registry
ALL_STYLES[query] = style;
list.forEach(mq => ALL_STYLES[mq] = style);

} catch (e) {
console.error(e);
}
}
}

/**
* Always convert to unique list of queries; for iteration in ::registerQuery()
*/
function normalizeQuery(mediaQuery: string | string[]): string[] {
return (typeof mediaQuery === 'undefined') ? [] :
(typeof mediaQuery === 'string') ? [mediaQuery] : unique(mediaQuery as string[]);
}

/**
* Filter duplicate mediaQueries in the list
*/
function unique(list: string[]): string[] {
let seen = {};
return list.filter(item => {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
}

5 changes: 2 additions & 3 deletions src/lib/media-query/media-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ export class MediaMonitor {
* and prepare for immediate subscription notifications
*/
private _registerBreakpoints() {
this._breakpoints.items.forEach(bp => {
this._matchMedia.registerQuery(bp.mediaQuery);
});
let queries = this._breakpoints.sortedItems.map(bp => bp.mediaQuery);
this._matchMedia.registerQuery(queries);
}
}
2 changes: 1 addition & 1 deletion src/lib/media-query/observable-media-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {ObservableMedia, MediaService} from './observable-media';
export function OBSERVABLE_MEDIA_PROVIDER_FACTORY(parentService: ObservableMedia,
matchMedia: MatchMedia,
breakpoints: BreakPointRegistry) {
return parentService || new MediaService(matchMedia, breakpoints);
return parentService || new MediaService(breakpoints, matchMedia);
}
/**
* Provider to return global service for observable service for all MediaQuery activations
Expand Down
26 changes: 14 additions & 12 deletions src/lib/media-query/observable-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ export class MediaService implements ObservableMedia {
*/
public filterOverlaps = true;

constructor(private mediaWatcher: MatchMedia,
private breakpoints: BreakPointRegistry) {
this.observable$ = this._buildObservable();
constructor(private breakpoints: BreakPointRegistry,
private mediaWatcher: MatchMedia) {
this._registerBreakPoints();
this.observable$ = this._buildObservable();
}

/**
Expand Down Expand Up @@ -122,22 +122,19 @@ export class MediaService implements ObservableMedia {
* mediaQuery activations
*/
private _registerBreakPoints() {
this.breakpoints.items.forEach((bp: BreakPoint) => {
this.mediaWatcher.registerQuery(bp.mediaQuery);
return bp;
});
let queries = this.breakpoints.sortedItems.map(bp => bp.mediaQuery);
this.mediaWatcher.registerQuery(queries);
}

/**
* Prepare internal observable
* NOTE: the raw MediaChange events [from MatchMedia] do not contain important alias information
* these must be injected into the MediaChange
*
* 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() {
const self = this;
// 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
const activationsOnly = (change: MediaChange) => {
return change.matches === true;
};
Expand All @@ -149,6 +146,11 @@ export class MediaService implements ObservableMedia {
return !bp ? true : !(self.filterOverlaps && 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
*/
return this.mediaWatcher.observe()
.filter(activationsOnly)
.map(addAliasInformation)
Expand Down

1 comment on commit 00ac57a

@hamzamihaidanielx
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

Please sign in to comment.