Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

fix(ObservableMedia): startup should propagate lastReplay value properly #313

Merged
merged 1 commit into from
Jun 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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$);
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the $ for in the variable name? If it's for private but cannot be marked as such for Angular reasons, we use an underscore to preface the variable

Copy link
Contributor Author

@ThomasBurleson ThomasBurleson Jun 13, 2017

Choose a reason for hiding this comment

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

The $ suffix is a community convention to indicate a variable reference that is actually an observable to future value(S)... originally seen in use by Andre Stalz (2 yrs ago).

I believe this is used consistently throughout the library.

}

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